<html>
<head>
  <title>21.策略（Strategy）模式</title>
  <basefont face="微软雅黑" size="2" />
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
  <meta name="exporter-version" content="YXBJ Windows/602628 (zh-CN, DDL); Windows/10.0.0 (Win64); EDAMVersion=V2;"/>
  <style>
    body, td {
      font-family: 微软雅黑;
      font-size: 10pt;
    }
  </style>
</head>
<body>
<a name="26414"/>
<h1>21.策略（Strategy）模式</h1>
<div>
<table bgcolor="#D4DDE5" border="0">
<tr><td><b>创建时间：</b></td><td><i>2020/10/4 6:07</i></td></tr>
<tr><td><b>更新时间：</b></td><td><i>2020/10/4 9:37</i></td></tr>
<tr><td><b>作者：</b></td><td><i>YuGe</i></td></tr>
<tr><td><b>来源：</b></td><td><a href="https://github.com/PhycheLiu/DesignPattern_Study-Test"><i>https://github.com/PhycheLiu/DesignPattern_Study-Test</i></a></td></tr>
</table>
</div>
<br/>

<div>
<span><div><div><span style="font-size: 12pt; font-weight: bold;">一、基本描述</span></div><ul><li><div><span style="font-size: 16px;">定义一系列的算法，把它们一个个封装起来，并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。</span></div></li><li><div><span style="font-size: 16px;">又称为政策（Policy）模式</span></div></li><li><div><span style="font-size: 12pt;">策略模式是一种定义一系列算法的方法，从概念上来看，所有这些算法完成的都是相同的工作，只是实现不同，它可以以相同的方式调用所有的算法，减少了各种算法类与使用算法类之间的耦合。</span></div></li><li><div><span style="font-size: 12pt;">策略模式用于算法的自由切换和扩展，它是应用较为广泛的设计模式之一。策略模式对应于解决某一问题的一个算法族，允许用户从该算法族中任选一个算法来解决某一问题，同时可以方便地更换算法或者增加新的算法。只要涉及到算法的封装、复用和切换都可以考虑使用策略模式。</span></div></li><li><div><span style="font-size: 16px;">策略模式是对算法的封装，它把算法的责任和算法本身分隔开，委派给不同的对象管理。</span><span style="font-size: 12pt;">策略模式将算法的定义与使用分开，也就是将算法的行为和环境分开，将算法的定义放在专门的策略类中（即具体策略类），每一个策略类封装了一种实现算法，使用算法的环境类针对抽 象策略类进行编程，符合“依赖倒转原则”。</span></div></li><li><div><span style="font-size: 16px;">策略模式的本质是使用面向对象的继承和多态，非常容易理解和掌握。</span></div></li><li><div><span style="font-size: 12pt;">在策略模式中，可以定义一些独立的类来封装不同的算法，每一个类封装一种具体的算法，每一个封装算法的类都可以称之为一种策略(Strategy)，为了保证这些策略在使用时具有一致性，一般会提供一个抽象的策略类来做规则的定义，而每种算法则对应于一个具体策略类。</span></div></li><li><div><span style="font-size: 12pt;">在策略模式中，对环境类和抽象策略类的理解非常重要，环境类是需要使用算法的类。在一个系统中可以存在多个环境类，它们可能需要重用一些相同的算法。</span></div></li><li><div><span style="font-size: 16px;">适用性：</span></div></li><ul><li><div><span style="font-size: 12pt;">各个类只有在算法或行为上稍有不同的场景。</span><span style="font-size: 12pt;">许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。</span></div></li><li><div><span style="font-size: 12pt;">算法需要自由切换的场景。需要使用一个算法的不同变体。例如，你可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时，可以使用策略模式。又比如，</span><span style="font-size: 12pt;">算法始终在进化，特别是一些站在技术前沿的行业，连业务专家都无法给你保证这样的系统规则能够存在多长时间，在这种情况下应选用策略模式。</span></div></li></ul></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">一个系统需要动态地在几种算法中选择一种，那么可以将这些算法封装到一个个的具体算法类中，而这些具体算法类都是一个抽象算法类的子类。换言之，这些具体算法类均有统一的接口，根据“里氏代换原则”和面向对象的多态性，客户端可以选择使用任何一个具体算法类，并只需要维持一个数据类型是抽象算法类的对象。</span><br/></div><ul><ul><li><div><span style="font-size: 12pt;">需要屏蔽算法那规则的场景。算法使用客户不应该知道的数据，或者</span><span style="font-size: 12pt;">不希望客户端知道复杂的、与算法相关的数据结构。</span><span style="font-size: 12pt;">很多算法知道一个名字即可，传递相关数字到算法当中，算法反馈一个结果。</span><span style="font-size: 12pt;">可使用策略模式</span><span style="color: unset; font-family: unset; font-size: 12pt;">在具体策略类中封装算法与相关的数</span><span style="color: unset; font-family: unset; font-size: 12pt;">据结构，可以提高算法的保密性与安全性。</span></div></li></ul><ul><li><div><span style="font-size: 12pt;">一个类定义了多种行为，并且这些行为在这个类的操作中以多个条件语句的形式出现。</span><span style="font-size: 12pt;">此时，使用策略模式，</span><span style="color: unset; font-family: unset; font-size: 12pt;">将相关的条件分支移人它们各自的 ConcreteStrategy 类中以代替这些条件语句</span><span style="font-size: 12pt;">，就可以避免使用难</span><span style="color: unset; font-family: unset; font-size: 12pt;">以维护的多重条件选择语句。</span></div></li></ul><li><div><span style="font-size: 16px;">对于实现的考虑</span></div></li><ul><li><div><span style="font-size: 12pt;"><b>定义 Strategy 和 Context 接口</b>。Strategy 和 Context 接口必须使得 ConcreteStrategy 能够有效的访问它所需要的 Context 中的任何数据，反之亦然。</span></div></li><ul><li><div><span style="font-size: 12pt;">一种办法是让 Context 将数据放在参数中传递给 Strategy 操作。也就是说，将数据发送给 Strategy。这使得 Strategy 和 Context 解耦。但另一方面， Context 可能发送一些 Strategy 不需要的数据。</span></div></li><li><div><span style="font-size: 12pt;">另一种办法是让 Context 将自身作为一个参数传递给 Strategy，该 Strategy 再显式地向该 Context 请求数据。或者， Strategy 可以存储对它的 Context 的一个引用，这样根本不再需要传递任何东西。</span></div></li></ul></ul></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">这两种情况下， Strategy都可以请求到它所需要的数据。但现在 Context必须对它的数据定义一个更为精细的接口，这将 Strategy和 Context更紧密地耦合在一起。</span></div><ul><ul><li><div><span style="font-size: 12pt;"><b>将 Strategy 作为模板参数</b>。在 C++ 中，可利用模板机制用一个 Strategy 来配置一个类。然而这种技术仅当下面条件满足时才可以使用：</span></div></li><ul><li><div><span style="font-size: 12pt;">可以在编译时选择 Strategy</span></div></li><li><div><span style="font-size: 12pt;">它不需在运行时改变</span></div></li></ul></ul></ul><div style="margin-left: 80px;"><span style="font-size: 12pt;">在这种情况下，要被配置的类（如， Context）被定义为以一个 Strategy 类作为一个参数的模板类。</span></div><div style="margin-left: 80px;"><span style="font-size: 12pt;">使用模板不再需要定义给 Strategy 定义接口的抽象类。把 Strategy 作为一个模板参数也使得可以将一个 Strategy 和它的 Context 静态地绑定在一起，从而提高效率。</span></div><ul><ul><li><div><span style="font-size: 12pt;"><b>使 Strategy对象成为可选的。</b>如果即使在不使用额外的 Strategy 对象的情况下， Context 也还有意义的话，那么它还可以被简化。 Context 在访问某 Strategy 前先检查它是否存在，如果有，那么就使用它；如果没有，那么 Context 执行缺省的行为。这种方法的好处是客户根本不需要处理 Strategy 对象，除非它们不喜欢缺省的行为。【<i>C++ STL中很多函数都有这种行为特征</i>】</span></div></li><li><div><span style="font-size: 16px;">Strategy 对象通常是很好的轻量级对象，适合与 Flyweight 模式混合使用。</span></div></li><li><div><span style="font-size: 16px;">可以将策略模式同简单工厂模式结合：在 Context 类中实现创建具体算法的简单工厂。</span></div></li><li><div><span style="font-size: 16px;">策略模式是一个非常简单的模式，在项目中使用非常多，但是它单独使用的地方比较少，因为它有致命缺陷：所有策略都需要暴露出去，这样客户端才方便决定使用哪一个策略。在实际的项目中，一般使用工厂方法模式来实现策略类的声明。</span></div></li></ul><li><div><span style="font-size: 16px;">策略模式封装了变化：对于</span><span style="font-size: 12pt; color: unset; font-family: unset;">面向对象的编程，并不是类越多越好，类的划分是为了封装，但分类的基础是抽象，具有相同属性和功能的对象的抽象集合才是类。算法本身只是一种策略，最重要的是这些算法是随时都可能相互替换的，这就是变化点，而封装变化点是面向对象编程的一种很重要的思维方式。</span></div></li><li><div><span style="font-size: 12pt;">策略模式就是用来封装算法的，但在实践中，可以用它来封装几乎任何类型的规则，只要在分析过程中听到需要在不同时间应用不同的业务规则，就可以考虑使用策略模式处理这种变化的可能性。</span></div></li><li><div><span style="font-size: 16px;">如果系统中的一个策略家族的具体策略数量超过4个，则需要考虑使用混合模式，解决策略类膨胀和对外暴露的问题。</span></div></li><li><div><span style="font-size: 16px;">实际应用</span></div></li><ul><li><div><span style="font-size: 12pt;">Java SE 的容器布局管理</span></div></li><li><div><span style="font-size: 16px;">微软的演示项目 PetShop 4.0</span></div></li></ul></ul><div><span style="font-size: 12pt; font-weight: bold;">二、图例</span></div><div style="text-align: center;"><img src="21.策略（Strategy）模式_files/Image.png" type="image/png" data-filename="Image.png"/><br/></div><ul><li><div><span style="font-size: 16px; font-weight: bold;">Strategy（策略）</span><span style="font-size: 16px;">：定义所有支持的算法的公共接口。Context 使用这个接口来调用某 ConcreteStrategy 定义的算法。是所有策略的父类，可以使抽象类或具体类，也可以是接口。是算法家族的抽象，定义每个算法或策略必须具有的方法和属性。</span></div></li><li><div><span style="font-size: 16px;"><b>ConcreteStrategy（具体策略）</b>：以 Strategy 接口实现某具体算法。实现抽象策略中的操作，含有具体的算法。在运行时，具体策略类将覆盖在环境类中定义的抽象策略类对象，使用一种具体的算法实现某个业务处理。</span></div></li><li><div><span style="font-size: 16px;"><b>Context（上下文）</b>：用一个 ConcreteStrategy 对象来配置。维护一个对 Strategy 对象的引用，用于定义所采用的策略。可定义一个接口来让 Strategy 访问它的数据。让客户与具体算法隔离，指定一个具体策略，保证策略可以互相替换。起到承上启下的封装作用，屏蔽高层模块对策略、算法的直接访问，封装可能存在的变化。Context 是策略模式的重点，借用了代理模式的思路，与代理模式的差别就是策略模式的封装角色和被封装的策略类不用是同一个接口。环境类是使用算法的角色，它在解决某个问题（即实现某个方法）时可以采用多种策略。</span></div></li><li><div><span style="font-size: 12pt; font-weight: bold;">协作：</span></div></li><ul><li><div><span style="font-size: 12pt;">Strategy 和 Context 相互作用以实现选定的算法。当算法被调用时， Context 可以将该算法所需要的所有数据都传递给该 Stategy。或者， Context 可以将自身作为一个参数传递给 Strategy 操作。这就让 Strategy在需要时可以回调 Context</span></div></li><li><div><span style="font-size: 12pt;">Context 将它的客户的请求转发给它的 Strategy。客户通常创建并传递一个 ConcreteStrategy 对象给该 Context；这样，客户仅与 Context 交互。通常有一系列的 ConcreteStrategy类可供客户从中选择。</span></div></li></ul></ul><div><span style="font-size: 12pt; font-weight: bold;">三、优缺点</span></div><div><span style="font-size: 12pt; font-weight: bold;">优点</span></div><ul><li><div><span style="font-size: 12pt;"><b>定义了一个算法系列</b>。Strategy 类层次为 Context 定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。</span><span style="font-size: 12pt;">策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族，恰当使用继承可以把公共的代码移到抽象策略类中，从而避免重复的代码。</span></div></li><li><div><span style="font-size: 12pt;"><b>一个替代继承的方法</b>。继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个 Context 类的子类，从而给它以不同的行为。但这会将行为硬行编制到 Context 中，而将算法的实现与 Context 的实现混合起来，从而使 Context 难以理解、难以维护和难以扩展，也就是说，这种实现将算法的使用和算法本身混合在一起，从而不可能再独立演化</span><span style="font-size: 12pt;">，而且使用继承无法实现算法或行为在程序运行时的动态切换</span><span style="font-size: 12pt;">。最后你得到一堆相关的类，它们之间的唯一差别是它们所使用的算法或行为。将算法封装在独立的 Strategy类中使得你可以独立于其 Context改变它，使它易于切换、易于理解、易于扩展。【<i>实质上就是增加了一个 Context 作为间接类</i>】</span></div></li><li><div><span style="font-size: 12pt;"><b>消除了一些条件语句</b>。Strategy 模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时，很难避免使用条件语句来选择合适的行为。使用策略模式后，可以由其他模块决定采用何种策略，策略家族对外提供的访问接口就是封装类，将行为封装在一个个独立的 Strategy 类中消除了这些条件语句。</span></div></li><li><div><span style="font-size: 16px;"><b>可灵活选择具体实现</b>。</span><span style="font-size: 12pt;">Strategy 模式可以提供相同行为的不同实现。客户可以根据不同时间/空间权衡取舍要求从不同策略中进行选择。</span></div></li></ul><div style="margin-left: 40px;"><font style="font-size: 12pt;"><b>算法可以自由切换</b>。</font><span style="font-size: 12pt;">只要实现抽象策略，它就成为策略家族的一个成员，通过封</span><span style="font-size: 12pt; color: unset; font-family: unset;">装角色对其进行封装，保证对外提供“可自由切换”的策略。</span></div><ul><li><div><span style="font-size: 16px;"><b>简化了单元测试</b>。每个算法都有自己的类，可以通过自己的接口单独测试每个算法可保证自身没有错误，修改其中的任何一个算法时，也不会影响其他算法。</span></div></li><li><div><span style="font-size: 16px;"><b>扩展性良好</b>。</span><span style="font-size: 12pt;">提供了对“开闭原则”的完美支持，用户可以在不修改原有系统的基础上选择算法或行为，也可以灵活地增加新的算法或行为。</span></div></li><li><div><span style="font-size: 16px;">提供了一种算法的复用机制。</span><span style="font-size: 12pt;">由于将算法单独提取出来封装在策略类中，因此不</span><span style="font-size: 12pt; color: unset; font-family: unset;">同的环境类可以方便地复用这些策略类。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">缺点</span></div><ul><li><div><span style="font-size: 12pt;"><b>客户必须了解不同的 Strategy。</b>本模式有一个潜在的缺点，就是一个客户要选择一个合适的 Strategy就必须知道这些 Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时，才需要使用 Strategy模式。</span></div></li></ul><div style="margin-left: 40px;"><span style="font-size: 12pt;">在基本的策略模式中，选择所用具体实现的职责由客户端对象承担，并转给策略模式的 Context 对象。客户端必须知道所有的策略类，并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别，以便适时选择恰当的算法，这本身并没有解除客户端需要选择判断的压力，而策略模式与简单工厂模式结合后，选择具体实现的职责也可以由 Context 来承担，这就最大化地减轻了客户端的职责。</span><br/></div><div style="margin-left: 40px;"><font style="font-size: 12pt;"><b>所有策略类均需要对外暴露</b>。</font><span style="font-size: 12pt;">上层模块必须知道有哪些策略，然后才能决定使用哪一个策略，这与迪米特法则是相违</span><span style="font-size: 12pt; color: unset; font-family: unset;">背的：想要使用某一个具体的策略类，必须先要了解这个策略。</span></div><ul><li><div><span style="font-size: 12pt;"><b>Strategy 和 Context 之间的通信开销。</b>无论各个 ConcreteStrategy 实现的算法是简单还是复杂，它们都共享 Strategy 定义的接口。因此很可能某些 ConcreteStrategy 不会都用到所有通过这个接口传递给它们的信息；简单的 ConcreteStrategy 可能不使用其中的任何信息！这就意味着有时 Context 会创建和初始化一些永远不会用到的参数。</span></div></li><li><div><span style="font-size: 12pt;"><b>增加了对象的数目。</b>每一个策略都是一个类，任何细小的变化都将导致系统要增加一个新的具体策略类，这些具体策略类复用的可能性很小，类的数量增多。Strategy 增加了一个应用中的对象的数目。可以结合享元模式（Flyweight）缓解这一问题。</span></div></li><li><div><span style="font-size: 12pt;"><b>无法同时在客户端使用多个策略类。</b>也就是说，在使用策略模式时，客户端每次只能使用一个策略类，不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。</span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">四、可能的改进</span></div><ul><li><div><span style="font-size: 16px;">可以使用策略枚举改进策略模式。【<i>但是个人对策略枚举持保留意见，只不过思路比较独特，但是实际上《设计模式之禅》这本书中对于策略枚举的例子是由问题的，所以个人认为可以简单的了解一下思路即可</i>】</span></div></li><li><div><span style="font-size: 16px;">可以与 Flyweight、工厂方法、简单工厂等模式混合使用。</span></div></li><li><div><span style="color: rgb(36, 41, 46); font-family: -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Helvetica, Arial, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;; font-size: 16px; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;"><a href="https://github.com/PhycheLiu/DesignPattern_Study-Test/tree/master/Java/src/com/company/strategy/sample01">可以利用“xml + 反射”机制改进策略模式。在 xml 当中指定使用哪个具体策略。</a></span></div></li></ul><div><span style="font-size: 12pt; font-weight: bold;">五、个人理解</span></div><ul><li><div><span style="font-size: 16px;">策略模式实现与思路非常简单。而且没有太多的变种，改进的总体思路是与其他设计模式混合使用。</span></div></li><li><div><span style="font-size: 16px;">在以前的实践当中，很多编码可以选择策略模式，且很多已经使用了策略模式。</span></div></li><li><div><span style="font-size: 16px;">每次遇到多条件语句时，均可想想是否可以选用设计模式替换：</span></div></li><ul><li><div><span style="font-size: 16px;">与状态相关的可以考虑选用状态模式</span></div></li><li><div><span style="font-size: 16px;">与状态无关的可以考虑选用策略模式</span></div></li></ul><li><div><span style="font-size: 16px;">单独使用策略模式固然好，但是最希望的还是结合策略模式实现动态的算法添加，如在C++中使用动态加载的动态链接库，Java 中使用“xml+反射机制”实现动态选择算法。</span></div></li><li><div><span style="font-size: 16px;">策略是特殊的代理（应该是指 Context 类），是面向对象中继承的应用（应该是指 Strategy 体系），其中核心是 Context，在算法与客户之间增加了一重间接性，使得实现更灵活。</span></div></li></ul><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br clear="none"/></div><div><br/></div><div><br/></div></div></span>
</div></body></html> 